//  Copyright (c) Facebook, Inc. and its affiliates.
//
// This source code is licensed under the MIT license found in the
// LICENSE file in the root directory of this source tree.

package com.facebook.react.tests.core;

import static org.fest.assertions.api.Assertions.assertThat;

import android.app.Instrumentation;
import androidx.test.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
import android.util.DisplayMetrics;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import com.facebook.react.ReactPackage;
import com.facebook.react.ReactRootView;
import com.facebook.react.bridge.JavaScriptModule;
import com.facebook.react.bridge.ModuleSpec;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.shell.MainReactPackage;
import com.facebook.react.testing.StringRecordingModule;
import com.facebook.react.testing.rule.ReactNativeTestRule;
import com.facebook.react.uimanager.PixelUtil;
import java.util.ArrayList;
import java.util.List;
import java.util.HashMap;
import java.util.Map;
import javax.inject.Provider;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import com.facebook.react.module.model.ReactModuleInfo;
import com.facebook.react.module.model.ReactModuleInfoProvider;
import com.facebook.react.module.annotations.ReactModule;

@RunWith(AndroidJUnit4.class)
public class ReactRootViewTest {

  private interface ReactRootViewTestModule extends JavaScriptModule {
    void setHeight(int height);
  }

  final StringRecordingModule mRecordingModule = new StringRecordingModule();
  final ReactPackage mReactPackage = new MainReactPackage() {
    @Override
    public NativeModule getModule(String name, ReactApplicationContext context) {
      if (name.equals(StringRecordingModule.NAME)) {
        return mRecordingModule;
      }

      return super.getModule(name, context);
    }

    @Override
    public ReactModuleInfoProvider getReactModuleInfoProvider() {
      final ReactModuleInfoProvider provider = super.getReactModuleInfoProvider();

      return new ReactModuleInfoProvider() {
        private Map<String, ReactModuleInfo> mModuleInfos = null;

        @Override
        public Map<String, ReactModuleInfo> getReactModuleInfos() {
          if (mModuleInfos != null) {
            return mModuleInfos;
          }

          mModuleInfos = new HashMap<>();
          mModuleInfos.putAll(provider.getReactModuleInfos());

          Class<? extends NativeModule> moduleClass = StringRecordingModule.class;
          ReactModule reactModule = moduleClass.getAnnotation(ReactModule.class);

          mModuleInfos.put(
            reactModule.name(),
            new ReactModuleInfo(
              reactModule.name(),
              moduleClass.getName(),
              reactModule.canOverrideExistingModule(),
              reactModule.needsEagerInit(),
              reactModule.hasConstants(),
              reactModule.isCxxModule(),
              false));

          return mModuleInfos;
        }
      };
    }
  };

  @Rule
  public ReactNativeTestRule mReactNativeRule =
    new ReactNativeTestRule("AndroidTestBundle.js", mReactPackage);

  @Before
  public void setup() {
    mReactNativeRule.render("CatalystRootViewTestApp");
  }

  @Test
  public void testResizeRootView() {
    final ReactRootView rootView = mReactNativeRule.getView();
    final View childView = rootView.getChildAt(0);

    assertThat(rootView.getWidth()).isEqualTo(childView.getWidth());

    final int newWidth = rootView.getWidth() / 2;

    Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
    instrumentation.runOnMainSync(
      new Runnable() {
        @Override
        public void run() {
          rootView.setLayoutParams(new FrameLayout.LayoutParams(
            newWidth,
            ViewGroup.LayoutParams.MATCH_PARENT));
        }
      });

    instrumentation.waitForIdleSync();
    mReactNativeRule.waitForIdleSync();

    assertThat(newWidth).isEqualTo(childView.getWidth());
  }

  @Test
  public void testRootViewWrapContent() {
    final ReactRootView rootView = mReactNativeRule.getView();

    Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
    instrumentation.runOnMainSync(
      new Runnable() {
        @Override
        public void run() {
          rootView.setLayoutParams(
              new FrameLayout.LayoutParams(
                  ViewGroup.LayoutParams.WRAP_CONTENT,
                  ViewGroup.LayoutParams.WRAP_CONTENT));
        }
      });

    instrumentation.waitForIdleSync();
    mReactNativeRule.waitForIdleSync();

    int newComponentHeight = 500;
    mReactNativeRule
      .getContext()
      .getJSModule(ReactRootViewTestModule.class)
      .setHeight(newComponentHeight);

    instrumentation.waitForIdleSync();
    mReactNativeRule.waitForIdleSync();
    instrumentation.waitForIdleSync();

    // added 0.5 to account for rounding issues
    assertThat(rootView.getMeasuredHeight())
      .isEqualTo((int) (PixelUtil.toPixelFromDIP(newComponentHeight) + 0.5));
  }

  /**
   * Verify that removing the root view from hierarchy will trigger subviews removal both on JS and
   * native side
   */
  @Test
  public void testRemoveRootView() {
    final ReactRootView rootView = mReactNativeRule.getView();

    assertThat(rootView.getChildCount()).isEqualTo(1);

    Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
    instrumentation.runOnMainSync(
      new Runnable() {
        @Override
        public void run() {
          ViewGroup parent = (ViewGroup) rootView.getParent();
          parent.removeView(rootView);
          // removing from parent should not remove child views, child views should be removed as
          // an effect of native call to UIManager.removeRootView
          assertThat(rootView.getChildCount()).isEqualTo(1);
        }
      });

    instrumentation.waitForIdleSync();
    mReactNativeRule.waitForIdleSync();

    assertThat(mRecordingModule.getCalls().size())
      .isEqualTo(0)
      .overridingErrorMessage("root component should not be automatically unmounted");
    assertThat(rootView.getChildCount()).isEqualTo(1);

    instrumentation.runOnMainSync(
      new Runnable() {
        @Override
        public void run() {
          rootView.unmountReactApplication();
        }
      });
    mReactNativeRule.waitForIdleSync();

    assertThat(mRecordingModule.getCalls().size()).isEqualTo(1);
    assertThat(mRecordingModule.getCalls().get(0)).isEqualTo("RootComponentWillUnmount");
    assertThat(rootView.getChildCount()).isEqualTo(0);
  }
}
